﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Xml;
using Microsoft.TeamFoundation.Server;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.VersionControl.Client;

namespace TfsBacklogMgr.Tfs
{
    internal class TfsStoredQuery
    {
        public Guid Id;
        public string Name;
        public TfsStoredQuery(Guid id, string name)
        {
            Id = id;
            Name = name;
        }

        public override string ToString()
        {
            return Name;
        }
    }


    internal class TfsWorkItem
    {
        public int Id;
        public string Name;
        public TfsWorkItem(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public override string ToString()
        {
            return Name;
        }
    }


    
    internal class TfsServer
    {
        #region Constructors and member variables

        private static TfsBacklogMgr.Main.Logger logger = TfsBacklogMgr.Main.LogManager.GetLogger("TfsServer");

        private const string NO_CREDENTIALS = "N/A";
        private const string STRING_FORMAT = "Server: {0}; Project: {1}; Credentials: {2}";
        private readonly ConnectionInfo _ConnectionInfo;
        private readonly ICredentials _Credentials;
        private TfsTeamProjectCollection _Server;

        internal TfsServer(ConnectionInfo connectionInfo)
        {
            _ConnectionInfo = connectionInfo;
            _ConnectionInfo.LastStatus = ConnectionStatus.Connecting;
            _ConnectionInfo.LastStatus = CheckConnectionStatus();
        }

        internal TfsServer(ConnectionInfo connectionInfo, ICredentials credentials)
        {
            _ConnectionInfo = connectionInfo;
            _ConnectionInfo.LastStatus = ConnectionStatus.Connecting;
            _Credentials = credentials;
            _ConnectionInfo.LastStatus = CheckConnectionStatus();
        }

        public static ConnectionInfo SelectProject()
        {
            TeamProjectPicker picker = new TeamProjectPicker( TeamProjectPickerMode.SingleProject, false );
            var result = picker.ShowDialog();
            if (result == System.Windows.Forms.DialogResult.OK && picker.SelectedProjects.Length > 0)
            {
                var conn = new ConnectionInfo();
                conn.TeamProjectCollectionUri = picker.SelectedTeamProjectCollection.Uri;
                conn.TeamProject = picker.SelectedProjects[0].Name;
                return conn;
            }
            return null;
        }
        #endregion

        #region Properties

        public string ServerUrl
        {
            get { return _ConnectionInfo.TeamProject; }
        }

        private TfsTeamProjectCollection Server
        {
            get
            {
                if (_Server == null)
                {
                    _Server = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(
                                                                                          _ConnectionInfo.
                                                                                              TeamProjectCollectionUri,
                                                                                          new UICredentialsProvider());
                }
                _Server.EnsureAuthenticated();

                return _Server;
            }
        }

        #endregion

        #region ITfsConnection Members

        public string ServerName
        {
            get { return _ConnectionInfo.TeamProjectCollectionUri.Host; }
        }

        public string ProjectName
        {
            get { return _ConnectionInfo.TeamProject; }
        }

        public IEnumerable<string> GetProjects()
        {
            var css = Server.GetService<ICommonStructureService>();

            return from projectInfo in css.ListAllProjects()
                   where projectInfo.Status == ProjectState.WellFormed
                   select projectInfo.Name;
        }


        /// <summary>
        /// Get a list of top level Areas from a team project. 
        /// </summary>
        /// <returns>A list of area names.</returns>
        public List<string> GetTopLevelAreas()
        {
            if (ProjectName == null) return null;

            var commonStructureService = Server.GetService<ICommonStructureService>();
            ProjectInfo projectInfo = commonStructureService.GetProjectFromName(ProjectName);
            NodeInfo[] nodes = commonStructureService.ListStructures(projectInfo.Uri);
            XmlElement areasXml = commonStructureService.GetNodesXml(new string[] { nodes[0].Uri }, true);
            XmlElement areas = areasXml.ChildNodes[0].ChildNodes[0] as XmlElement;

            List<string> result = new List<string>();
            if (areas != null)
            {
                foreach (XmlNode node in areas.ChildNodes)
                    result.Add(node.Attributes["Name"].Value);
            }
            return result;
        }

        /// <summary>
        /// Get a list of all Areas from a team project. 
        /// </summary>
        /// <returns>A list of short area paths.</returns>
        public List<string> GetAreas()
        {
            if (ProjectName == null)
                return null;

            var css = Server.GetService<ICommonStructureService>();
            ProjectInfo projectInfo = css.GetProjectFromName(ProjectName);
            NodeInfo[] nodes = css.ListStructures(projectInfo.Uri);
            XmlElement areasXml = css.GetNodesXml(new string[] { nodes[0].Uri }, true);
            XmlElement areas = areasXml.ChildNodes[0].ChildNodes[0] as XmlElement;

            List<string> result = new List<string>();

            if (areas != null)
            {
                result = GetSubNodes("", areas);
            }
            return result;
        }

        /// <summary>
        /// Get a list of top level iterations. 
        /// </summary>
        /// <returns>List of the root level iterations.</returns>
        public List<string> GetTopLevelIterations()
        {
            if (ProjectName == null)
                return null;

            var css = Server.GetService<ICommonStructureService>();
            ProjectInfo projectInfo = css.GetProjectFromName(ProjectName);
            NodeInfo[] nodes = css.ListStructures(projectInfo.Uri);
            XmlElement iterationsXml = css.GetNodesXml(new string[] { nodes[1].Uri }, true);
            XmlElement iterations = iterationsXml.ChildNodes[0].ChildNodes[0] as XmlElement;

            List<string> result = new List<string>();

            if (iterations != null)
            {
                foreach (XmlNode node in iterations.ChildNodes)
                    result.Add( node.Attributes["Name"].Value );
            }
            return result;
        }

        /// <summary>
        /// Get a list of all iterations. 
        /// </summary>
        /// <returns>List of the short iteration paths.</returns>
        public List<string> GetAllIterations()
        {
            List<string> result = new List<string>();
            if (ProjectName == null)
                return result;

            var css = Server.GetService<ICommonStructureService>();
            ProjectInfo projectInfo = css.GetProjectFromName(ProjectName);
            NodeInfo[] nodes = css.ListStructures(projectInfo.Uri);
            XmlElement iterationsXml = css.GetNodesXml(new string[] { nodes[1].Uri }, true);
            XmlElement iterations = iterationsXml.ChildNodes[0].ChildNodes[0] as XmlElement;

            if (iterations != null)
            {
                result = GetSubNodes( "$", iterations );
            }
            return result;
        }

        private List<string> GetSubNodes(string path, XmlElement root)
        {
            List<string> result = new List<string>();
            if (root == null)
                return result;
            foreach (XmlNode node in root.ChildNodes)
            {
                string name = node.Attributes["Name"].Value;
                if (! string.IsNullOrEmpty(path)) 
                    name = path + "\\" + name;
                result.Add( name );
                if (node.ChildNodes != null && node.ChildNodes.Count > 0 )
                {
                    List<string> subresults = GetSubNodes(name, node.FirstChild as XmlElement);
                    result.AddRange(subresults);
                }
            }
            return result;
        }

        private string NormalizeAreaPath( string shortAreaPath )
        {
            string result = shortAreaPath;
            if( ! shortAreaPath.Contains( ProjectName ) )
            {
                result = ProjectName + "\\" + shortAreaPath;
            }
            return result;
        }

        private string NormalizeIterationPath(string shortIterationPath)
        {
            string result = shortIterationPath;
            if (!shortIterationPath.Contains(ProjectName))
            {
                result =  ProjectName + "\\" + shortIterationPath;
            }
            return result;
        }

#if false
        public IEnumerable<string> GetWorkItemTypeDefinitions()
        {
            var store = Server.GetService<WorkItemStore>();
            Project project = store.Projects[_ConnectionInfo.TeamProject];

            return from WorkItemType item in project.WorkItemTypes
                   select item.Name;
        }


        public string GetWorkItem(int selectedWorkItem)
        {
            var store = Server.GetService<WorkItemStore>();
            var workItem = store.GetWorkItem(selectedWorkItem);

            return null; // wi -> XML
        }

        public string GetWorkItemTypeDefinition(string witdName)
        {
            var store = Server.GetService<WorkItemStore>();
            Project project = store.Projects[_ConnectionInfo.TeamProject];

            //store.GetWorkItem()
            XmlDocument doc = project.WorkItemTypes[witdName].Export(true);

            return doc.OuterXml;
        }

#endif
        #endregion

        #region TFS Workitems

        public Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem GetWorkItem(int id)
        {
            logger.Trace("GetWorkItem " + id);
            var store = Server.GetService<WorkItemStore>();
            return store.GetWorkItem(id);
        }

        private static void ThrowArgException(System.Collections.ArrayList badFields)
        {
            string msg = "";
            foreach (var badArg in badFields)
            {
                Field f = badArg as Field;
                if (f != null)
                    msg = msg + f.Name + ":" + f.Status + "\r\n";
            }
            throw new ArgumentException("Validation failed", msg);
        }

        public int CreateUserStoryItem(string title, string areaPath, string iterPath, string assignedTo, string so_product)
        {
            logger.Trace("CreateUserStoryItem: " + title);
            var store = Server.GetService<WorkItemStore>();
            Project teamproject = store.Projects[ProjectName];
            if (teamproject == null)
            {
                logger.Trace("CreateUserStoryItem: no project '" + ProjectName + "' found.");
                return 0;
            }
            WorkItemType wit = teamproject.WorkItemTypes["User Story"];
            if (wit == null)
            {
                logger.Trace("CreateUserStoryItem: no work item type found.");
                return 0;
            }

            WorkItem item = new WorkItem(wit);
            item.Title = SanitizeLine(title);
            item.IterationPath = NormalizeIterationPath(iterPath);
            item.AreaPath = NormalizeAreaPath(areaPath);
            item.Fields["SO.Product"].Value = so_product;

            var badFields = item.Validate();
            if (badFields.Count > 0)
                badFields = TryToRepairAssignedTo(assignedTo, item, badFields);
            if (badFields.Count == 0)
                item.Save();
            else
                ThrowArgException(badFields);

            return item.Id;
        }

        public int CreateTestCaseItem(string title, string descr, string areaPath, string iterPath, string assignedTo, string so_product)
        {
            var store = Server.GetService<WorkItemStore>();
            Project teamproject = store.Projects[ProjectName];
            if (teamproject == null)
                return 0;
            WorkItemType wit = teamproject.WorkItemTypes["Test Case"];
            if (wit == null)
                return 0;

            WorkItem item = new WorkItem(wit);
            item.Title = SanitizeLine(title);
            item.Description = descr;
            item.IterationPath = NormalizeIterationPath(iterPath);
            item.AreaPath = NormalizeAreaPath(areaPath);
            item.Fields["SO.Product"].Value = so_product;

            var badFields = item.Validate();
            // try to repair assign-to
            if (badFields.Count > 0)
                badFields = TryToRepairAssignedTo(assignedTo, item, badFields);
            
            if (badFields.Count == 0)
                item.Save();
            else
                ThrowArgException(badFields);

            return item.Id;
        }

        private System.Collections.ArrayList TryToRepairAssignedTo(string assignedTo, WorkItem item, System.Collections.ArrayList badFields)
        {
            System.Collections.ArrayList retVal = null;
            foreach (object aField in badFields)
            { 
                Field badField = aField as Field;
                if (badField.ReferenceName == "System.AssignedTo")
                {
                    item.Fields["System.AssignedTo"].Value = assignedTo;
                    retVal = item.Validate();
                }
            }
            return retVal == null ? badFields : retVal;
        }


        private string SanitizeLine(string s)
        {
            s = s.Replace('\t', ' ');
            s = s.Replace('\n', ' ');
            s = s.Replace('\r', ' ');
            return s;
        }

        /// <summary>
        /// Create a link to relate one work item to hierarchically, i.e. to make one item the parent of the other.
        /// </summary>
        /// <param name="childId">The child work item.</param>
        /// <param name="parentId">The parent work item.</param>
        public void CreateWorkItemParentChildLink( int parentId, int childId)
        {
            WorkItemStore wiStore = Server.GetService<WorkItemStore>();
            WorkItem parent = wiStore.GetWorkItem(parentId);
            WorkItemLinkType linkType = wiStore.WorkItemLinkTypes["System.LinkTypes.Hierarchy"];
            WorkItemLinkTypeEnd linkTypeEnd = wiStore.WorkItemLinkTypes.LinkTypeEnds[linkType.ForwardEnd.Name];
            parent.Links.Add(new RelatedLink(linkTypeEnd, childId));
            var badFields = parent.Validate();
            if (badFields.Count == 0)
                parent.Save();
            else
                ThrowArgException(badFields);
        }

        /// <summary>
        /// Create a link to relate one work item to hierarchically, i.e. to make one item the parent of the other.
        /// </summary>
        /// <param name="userStoryId">The user storywork item.</param>
        /// <param name="testCaseId">The parent work item.</param>
        public void CreateWorkItemTestedByLink(int userStoryId, int testCaseId)
        {
            WorkItemStore wiStore = Server.GetService<WorkItemStore>();
            WorkItem userStory = wiStore.GetWorkItem(userStoryId);
            WorkItemLinkType linkType = wiStore.WorkItemLinkTypes["Microsoft.VSTS.Common.TestedBy"];
            WorkItemLinkTypeEnd linkTypeEnd = wiStore.WorkItemLinkTypes.LinkTypeEnds[linkType.ForwardEnd.Name];
            userStory.Links.Add(new RelatedLink(linkTypeEnd, testCaseId));
            var badFields = userStory.Validate();
            if( badFields.Count == 0 )
                userStory.Save();
            else
                ThrowArgException(badFields);
        }

        #endregion

        #region Spec Query

        public List<TfsStoredQuery> GetStoredQueryNames()
        {
            List<TfsStoredQuery> result = new List<TfsStoredQuery>();

            WorkItemStore store = Server.GetService<WorkItemStore>();
            Project teamproject = store.Projects[ProjectName];
            var queryHierarchy = teamproject.QueryHierarchy;

            if (queryHierarchy.SupportsFolders)
            {
                result = GetStoredQueryNamesInFolder("", "", queryHierarchy);
            }

            return result;
        }

        private List<TfsStoredQuery> GetStoredQueryNamesInFolder(string indent, string prefix, QueryFolder folder)
        {
            List<TfsStoredQuery> result = new List<TfsStoredQuery>();

            foreach( var item in folder )
            {
                QueryFolder queryFolder = item as QueryFolder;
                if (queryFolder != null)
                {
                    List<TfsStoredQuery> subresult = GetStoredQueryNamesInFolder(indent + "  ", queryFolder.Name, queryFolder);
                    result.AddRange(subresult);
                }
                else
                {
                    Guid id = item.Id;
                    string name = prefix + ": " + item.Name;
                    result.Add(new TfsStoredQuery(id, name));
                }
            }
            return result;
        }

        public List<TfsWorkItem> GetStoredQueryResults(TfsStoredQuery queryItem)
        {
            List<TfsWorkItem> result = new List<TfsWorkItem>();

            WorkItemStore store = Server.GetService<WorkItemStore>();
            QueryDefinition queryDef = store.GetQueryDefinition(queryItem.Id);
            Project teamproject = store.Projects[ProjectName];
            string queryText = queryDef.QueryText;
            var queryParams = new Dictionary<string, string>();
            queryParams.Add("project", ProjectName);
            Query query = new Query(store, queryText, queryParams);
            WorkItemCollection workitems;
            if (query.IsLinkQuery || query.IsTreeQuery)
            {
            }
            else
            {
                workitems = query.RunQuery();
                foreach (WorkItem workitem in workitems)
                {
                    result.Add(new TfsWorkItem(workitem.Id, workitem.Title));
                }
            }
            return result;
        }

        #endregion

        public override string ToString()
        {
            return string.Format(STRING_FORMAT,
                                 _ConnectionInfo.TeamProjectCollectionUri,
                                 _ConnectionInfo.TeamProject,
                                 _Credentials == null ? NO_CREDENTIALS : _Credentials.ToString());
        }


        internal ConnectionStatus CheckConnectionStatus()
        {
            try
            {
                Server.Connect(Microsoft.TeamFoundation.Framework.Common.ConnectOptions.IncludeServices);
                Server.EnsureAuthenticated();
            }
            catch
            {
                return ConnectionStatus.Error;
            }
            if (Server.HasAuthenticated)
                return ConnectionStatus.Online;
            return ConnectionStatus.Offline;
        }

        internal WorkItemCollection GetAllWorkItems()
        {
            WorkItemCollection result = null;
            string wiql = string.Format("SELECT * FROM WorkItems WHERE System.TeamProject='{0}'", ProjectName);

            WorkItemStore store = Server.GetService<WorkItemStore>();
            Project teamproject = store.Projects[ProjectName];
            result = store.Query( wiql );
            return result;
        }

        // return dictionary: login -> display name
        internal Dictionary<string,string> GetAllUsers()
        {
            Dictionary<string, string> result = new Dictionary<string, string>();

            VersionControlServer versionControl = Server.GetService<VersionControlServer>();

            TeamProject teamproject = versionControl.GetTeamProject(ProjectName); 
            IGroupSecurityService sec = Server.GetService<IGroupSecurityService>();
            Identity[] appGroups = sec.ListApplicationGroups( teamproject.ArtifactUri.AbsoluteUri);
            foreach (Identity group in appGroups)
            {
                Identity[] groupMembers = sec.ReadIdentities(SearchFactor.Sid, new string[] { group.Sid }, QueryMembership.Expanded);
                foreach (Identity member in groupMembers)
                {
                    System.Diagnostics.Debug.WriteLine(member.DisplayName);
                    if (member.Members != null)
                    {
                        foreach (string memberSid in member.Members)
                        {
                            try
                            {
                                Identity memberInfo = sec.ReadIdentity(SearchFactor.Sid, memberSid, QueryMembership.None);
                                result[memberInfo.AccountName] = memberInfo.DisplayName;
                                System.Diagnostics.Debug.WriteLine(" user {0} '{1}'", memberInfo.AccountName, memberInfo.DisplayName);
                            }
                            catch
                            { /* do nothing */ }
                        }
                    }
                }
            }
            return result;
        }



        #region Next States

        private static Dictionary<string, Dictionary<string, List<TfsBacklogMgr.Main.StateAndReason>>> _TypeToStateToStates;

        public List<TfsBacklogMgr.Main.StateAndReason> GetNextStatesFor(string WIT, string curState)
        {
            if (_TypeToStateToStates == null ||
                _TypeToStateToStates[WIT] == null)
                return new List<TfsBacklogMgr.Main.StateAndReason>();
            var states = _TypeToStateToStates[WIT];
            var result = states[curState];
            return result;
        }

        public void AddAllowedStates(WorkItem wi)
        {
            if( _TypeToStateToStates == null )
                _TypeToStateToStates = new Dictionary<string,Dictionary<string,List<TfsBacklogMgr.Main.StateAndReason>>>();

            if(!  _TypeToStateToStates.ContainsKey( wi.Type.Name ) )
            {
                WorkItem workItem = wi;
                var stateMap = new Dictionary<string, List<TfsBacklogMgr.Main.StateAndReason>>();

                // to find possible next states, the Work Item type definition (XML based format) is used.
                // get Work Item type
                WorkItemType wit = workItem.Type;
                // get Work Item type definition
                XmlDocument witd = wit.Export(true);
                // retrieve the transitions node
                XmlNode transitionsNode = witd.SelectSingleNode("descendant::TRANSITIONS");
                // for each transition definition (== possible next allowed state)
                foreach (XmlNode transitionNode in transitionsNode.SelectNodes("TRANSITION"))
                {
                    // if the transition contains a next allowed state
                    string fromState = transitionNode.Attributes.GetNamedItem("from").Value;
                    if (! stateMap.ContainsKey(fromState) )
                        stateMap[fromState] = new List<TfsBacklogMgr.Main.StateAndReason>();

                    // show the next allowed state
                    string toState = transitionNode.Attributes.GetNamedItem("to").Value;

                    // retrieve the reasons node
                    XmlNode reasonsNode = transitionNode.SelectSingleNode("REASONS");
                    // for each state change reason 
                    string defaultReason = "";
                    foreach (XmlNode reason in reasonsNode.ChildNodes)
                    {
                        if( defaultReason == "" )
                            defaultReason = reason.Attributes[0].Value;
                    }
                    var stateAndReason = new TfsBacklogMgr.Main.StateAndReason(toState, defaultReason);
                    stateMap[fromState].Add(stateAndReason);
                }
                _TypeToStateToStates[wi.Type.Name] = stateMap;
            }
        }

        #endregion


        internal TfsConfig DetermineConfig()
        {
            WorkItemStore store = Server.GetService<WorkItemStore>();
            Project teamproject = store.Projects[ProjectName];
            TfsConfig cfg = TfsConfig.GetTfsConfig( teamproject.WorkItemTypes );
            return cfg;
        }
        
    }
}
